//
// riot route 独立版本
//
;
(function(window, undefined) {


  var route = (function() {
    'use strict';
    /**
     * Simple client-side router
     * @module riot-route
     */

    var RE_ORIGIN = /^.+?\/\/+[^\/]+/;
    var EVENT_LISTENER = 'EventListener';
    var REMOVE_EVENT_LISTENER = 'remove' + EVENT_LISTENER;
    var ADD_EVENT_LISTENER = 'add' + EVENT_LISTENER;
    var HAS_ATTRIBUTE = 'hasAttribute';
    var POPSTATE = 'popstate';
    var HASHCHANGE = 'hashchange';
    var TRIGGER = 'emit';
    var MAX_EMIT_STACK_LEVEL = 3;
    var win = typeof window != 'undefined' && window;
    var doc = typeof document != 'undefined' && document;
    var hist = win && history;
    var loc = win && (hist.location || win.location);
    var prot = Router.prototype;
    var clickEvent = doc && doc.ontouchstart ? 'touchstart' : 'click';
    var central = win.pip.emitter();

    var started = false;
    var routeFound = false;
    var debouncedEmit;
    var base;
    var current;
    var parser;
    var secondParser;
    var emitStack = [];
    var emitStackLevel = 0;

    /**
     * Default parser. You can replace it via router.parser method.
     * @param {string} path - current path (normalized)
     * @returns {array} array
     */
    function DEFAULT_PARSER(path) {
      return path.split(/[/?#]/)
    }

    /**
     * Default parser (second). You can replace it via router.parser method.
     * @param {string} path - current path (normalized)
     * @param {string} filter - filter string (normalized)
     * @returns {array} array
     */
    function DEFAULT_SECOND_PARSER(path, filter) {
      var f = filter
        .replace(/\?/g, '\\?')
        .replace(/\*/g, '([^/?#]+?)')
        .replace(/\.\./, '.*');
      var re = new RegExp(("^" + f + "$"));
      var args = path.match(re);

      if (args) { return args.slice(1) }
    }

    /**
     * Simple/cheap debounce implementation
     * @param   {function} fn - callback
     * @param   {number} delay - delay in seconds
     * @returns {function} debounced function
     */
    function debounce(fn, delay) {
      var t;
      return function() {
        clearTimeout(t);
        t = setTimeout(fn, delay);
      }
    }

    /**
     * Set the window listeners to trigger the routes
     * @param {boolean} autoExec - see route.start
     */
    function start(autoExec) {
      debouncedEmit = debounce(emit, 1);
      win[ADD_EVENT_LISTENER](POPSTATE, debouncedEmit);
      win[ADD_EVENT_LISTENER](HASHCHANGE, debouncedEmit);
      doc[ADD_EVENT_LISTENER](clickEvent, click);
      if (autoExec) { emit(true); }
    }

    /**
     * Router class
     */
    function Router() {
      this.$ = [];
      win.pip.emitter(this); // make it emitter
      central.on('stop', this.s.bind(this));
      central.on('emit', this.e.bind(this));
    }

    function normalize(path) {
      return path.replace(/^\/|\/$/, '')
    }

    function isString(str) {
      return typeof str == 'string'
    }

    /**
     * Get the part after domain name
     * @param {string} href - fullpath
     * @returns {string} path from root
     */
    function getPathFromRoot(href) {
      return (href || loc.href).replace(RE_ORIGIN, '')
    }

    /**
     * Get the part after base
     * @param {string} href - fullpath
     * @returns {string} path from base
     */
    function getPathFromBase(href) {
      return base[0] === '#' ?
        (href || loc.href || '').split(base)[1] || '' :
        (loc ? getPathFromRoot(href) : href || '').replace(base, '')
    }

    function emit(force) {
      // the stack is needed for redirections
      var isRoot = emitStackLevel === 0;
      if (MAX_EMIT_STACK_LEVEL <= emitStackLevel) { return }

      emitStackLevel++;
      emitStack.push(function() {
        var path = getPathFromBase();
        if (force || path !== current) {
          central[TRIGGER]('emit', path);
          current = path;
        }
      });
      if (isRoot) {
        var first;
        while (first = emitStack.shift()) { first(); } // stack increses within this call
        emitStackLevel = 0;
      }
    }

    function click(e) {
      if (
        e.which !== 1 // not left click
        ||
        e.metaKey || e.ctrlKey || e.shiftKey // or meta keys
        ||
        e.defaultPrevented // or default prevented
      ) { return }

      var el = e.target;
      while (el && el.nodeName !== 'A') { el = el.parentNode; }

      if (!el || el.nodeName !== 'A' // not A tag
        ||
        el[HAS_ATTRIBUTE]('download') // has download attr
        ||
        !el[HAS_ATTRIBUTE]('href') // has no href attr
        ||
        el.target && el.target !== '_self' // another window or frame
        ||
        el.href.indexOf(loc.href.match(RE_ORIGIN)[0]) === -1 // cross origin
      ) { return }

      if (el.href !== loc.href &&
        (
          el.href.split('#')[0] === loc.href.split('#')[0] // internal jump
          ||
          base[0] !== '#' && getPathFromRoot(el.href).indexOf(base) !== 0 // outside of base
          ||
          base[0] === '#' && el.href.split(base)[0] !== loc.href.split(base)[0] // outside of #base
          ||
          !go(getPathFromBase(el.href), el.title || doc.title) // route not found
        )) { return }

      e.preventDefault();
    }

    /**
     * Go to the path
     * @param {string} path - destination path
     * @param {string} title - page title
     * @param {boolean} shouldReplace - use replaceState or pushState
     * @returns {boolean} - route not found flag
     */
    function go(path, title, shouldReplace) {
      // Server-side usage: directly execute handlers for the path
      if (!hist) { return central[TRIGGER]('emit', getPathFromBase(path)) }

      path = base + normalize(path);
      title = title || doc.title;
      // browsers ignores the second parameter `title`
      shouldReplace
        ?
        hist.replaceState(null, title, path) :
        hist.pushState(null, title, path);
      // so we need to set it manually
      doc.title = title;
      routeFound = false;
      emit();
      return routeFound
    }

    /**
     * Go to path or set action
     * a single string:                go there
     * two strings:                    go there with setting a title
     * two strings and boolean:        replace history with setting a title
     * a single function:              set an action on the default route
     * a string/RegExp and a function: set an action on the route
     * @param {(string|function)} first - path / action / filter
     * @param {(string|RegExp|function)} second - title / action
     * @param {boolean} third - replace flag
     */
    prot.m = function(first, second, third) {
      if (isString(first) && (!second || isString(second))) { go(first, second, third || false); } else if (second) { this.r(first, second); } else { this.r('@', first); }
    };

    /**
     * Stop routing
     */
    prot.s = function() {
      this.off('*');
      this.$ = [];
    };

    /**
     * Emit
     * @param {string} path - path
     */
    prot.e = function(path) {
      this.$.concat('@').some(function(filter) {
        var args = (filter === '@' ? parser : secondParser)(normalize(path), normalize(filter));
        if (typeof args != 'undefined') {
          this[TRIGGER].apply(null, [filter].concat(args));
          return routeFound = true // exit from loop
        }
      }, this);
    };

    /**
     * Register route
     * @param {string} filter - filter for matching to url
     * @param {function} action - action to register
     */
    prot.r = function(filter, action) {
      if (filter !== '@') {
        filter = '/' + normalize(filter);
        this.$.push(filter);
      }
      this.on(filter, action);
    };

    var mainRouter = new Router();
    var route = mainRouter.m.bind(mainRouter);

    /**
     * Create a sub router
     * @returns {function} the method of a new Router object
     */
    route.create = function() {
      var newSubRouter = new Router();
      // assign sub-router's main method
      var router = newSubRouter.m.bind(newSubRouter);
      // stop only this sub-router
      router.stop = newSubRouter.s.bind(newSubRouter);
      return router
    };

    /**
     * Set the base of url
     * @param {(str|RegExp)} arg - a new base or '#' or '#!'
     */
    route.base = function(arg) {
      base = arg || '#';
      current = getPathFromBase(); // recalculate current path
    };

    /** Exec routing right now **/
    route.exec = function() {
      emit(true);
    };

    /**
     * Replace the default router to yours
     * @param {function} fn - your parser function
     * @param {function} fn2 - your secondParser function
     */
    route.parser = function(fn, fn2) {
      if (!fn && !fn2) {
        // reset parser for testing...
        parser = DEFAULT_PARSER;
        secondParser = DEFAULT_SECOND_PARSER;
      }
      if (fn) { parser = fn; }
      if (fn2) { secondParser = fn2; }
    };

    /**
     * Helper function to get url query as an object
     * @returns {object} parsed query
     */
    route.query = function() {
      var q = {};
      var href = loc.href || current;
      href.replace(/[?&](.+?)=([^&]*)/g, function(_, k, v) { q[k] = v; });
      return q
    };

    /** Stop routing **/
    route.stop = function() {
      if (started) {
        if (win) {
          win[REMOVE_EVENT_LISTENER](POPSTATE, debouncedEmit);
          win[REMOVE_EVENT_LISTENER](HASHCHANGE, debouncedEmit);
          doc[REMOVE_EVENT_LISTENER](clickEvent, click);
        }
        central[TRIGGER]('stop');
        started = false;
      }
    };

    /**
     * Start routing
     * @param {boolean} autoExec - automatically exec after starting if true
     */
    route.start = function(autoExec) {
      if (!started) {
        if (win) {
          if (document.readyState === 'complete') { start(autoExec); }
          // the timeout is needed to solve
          // a weird safari bug https://github.com/riot/route/issues/33
          else {
            win[ADD_EVENT_LISTENER]('load', function() {
              setTimeout(function() { start(autoExec); }, 1);
            });
          }
        }
        started = true;
      }
    };

    /** Prepare the router **/
    route.base();
    route.parser();

    return route;

  }());


  if (typeof exports === 'object')
    module.exports = route
  else if (typeof define === 'function' && define.amd)
    define(function() { return route })
  else
    window.route = route

})(typeof window != 'undefined' ? window : undefined);
